"""
The MIT License (MIT)
Copyright © 2022 Walkline Wang (https://walkline.wang)
Gitee: https://gitee.com/walkline/performance-monitor-micropython
"""
from machine import SPI, Pin, UART, Timer
import utime
from sys import exit
import st7789
import os
import gc
import esp32
from libs.fontlib import FontLib
from config import Config

RESOURCE_PARTITION = '/res'
DEBUG = False
COLOR_ORANGE = 0xfde8
ROTATION = 0


def timed_function(f, *args, **kwargs):
	myname = str(f).split(' ')[1]
	def new_func(*args, **kwargs):
		if DEBUG:
			t = utime.ticks_us()
			result = f(*args, **kwargs)
			delta = utime.ticks_diff(utime.ticks_us(), t)
			print('Function {} | Time = {:6.3f}ms'.format(myname, delta/1000))
		else:
			result = f(*args, **kwargs)
		return result
	return new_func


class ResourceManager(object):
	@staticmethod
	@timed_function
	def mount_resource_partition() -> bool:
		try:
			os.umount(RESOURCE_PARTITION)
		except OSError as ose:
			if str(ose) == '[Errno 22] EINVAL':
				pass
			else:
				print(ose)

		res = esp32.Partition.find(esp32.Partition.TYPE_DATA, label='resource')

		if res:
			os.mount(res[0], RESOURCE_PARTITION)
			return True
		else:
			return False


class GroupInfo(object):
	'''
	显示窗格相关信息类
	'''
	def __init__(self, index:int, display:st7789.ST7789, title_char_width:int, title_char_height:int, content_char_width:int, content_char_height:int, gap:int=4):
		self.bound = None
		self.last_content = '100'

		self.frame_width = int(display.width() / 2)
		self.frame_height = int((display.height() - PerformanceMonitor.HEADER_HEIGHT) / 2)

		self.title_char_width = title_char_width
		self.title_char_height = title_char_height

		self.content_char_width = content_char_width
		self.content_char_height = content_char_height

		self.group_width = self.frame_width - gap * 2
		self.group_height = int(self.frame_height - gap * 2 - title_char_height / 2)

		if index == 0:
			self.frame_x = 0
			self.frame_y = PerformanceMonitor.HEADER_HEIGHT
		elif index == 1:
			self.frame_x = self.frame_width
			self.frame_y = PerformanceMonitor.HEADER_HEIGHT
		elif index == 2:
			self.frame_x = 0
			self.frame_y = PerformanceMonitor.HEADER_HEIGHT + self.frame_height
		elif index == 3:
			self.frame_x = self.frame_width
			self.frame_y = PerformanceMonitor.HEADER_HEIGHT + self.frame_height

		self.group_x = self.frame_x + gap
		self.group_y = self.frame_y + int(title_char_height / 2) + 5

		self.title_x = self.group_x + title_char_width
		self.title_y = self.group_y - int(title_char_height / 2)

		self.content_y = self.group_y + int((self.group_height - content_char_height) / 2) + gap


class PerformanceMonitor(object):
	CHARS_MONITOR_HEADER = 'Monitor' # 28
	CHARS_MONITOR_TIME = '0123456789:' # 20
	CHARS_MONITOR_TITLE = 'CPURAMHDDGPU' # 28
	CHARS_MONITOR_PERCENT = '%' # 20
	CHARS_MONITOR_CONTENT = '0123456789' # 50

	HEADER_HEIGHT = 30
	GROUP_CONTENT_OFFSET_X = 2
	TIME_CONTENT_OFFSET_Y = 2

	@timed_function
	def __init__(self, display:st7789.ST7789, font_folder:str):
		self.__uart = None
		self.__timer = Timer(0)
		self.__display = display
		self.__font_folder = font_folder

		self.__display_width = display.width()
		# self.__display_height = display.height()

		self.__last_time = '00:00:00'

		self.__initialize_fonts()

		self.draw_background()

		gc.collect()

	@timed_function
	def __initialize_fonts(self):
		'''
		初始化字库并提取需要的字符数据
		'''
		fontlib = FontLib(self.__font_folder + 'font_28.bin')
		self.__header_title_buffer_dict = fontlib.get_characters(
			PerformanceMonitor.CHARS_MONITOR_HEADER +
			PerformanceMonitor.CHARS_MONITOR_TITLE
		)
		self.__header_title_char_width = fontlib.font_width
		self.__header_title_char_height = fontlib.font_height

		fontlib = FontLib(self.__font_folder + 'font_20.bin')
		self.__time_percent_buffer_dict = fontlib.get_characters(
			PerformanceMonitor.CHARS_MONITOR_TIME +
			PerformanceMonitor.CHARS_MONITOR_PERCENT
		)
		self.__time_percent_char_width = fontlib.font_width
		self.__time_percent_char_height = fontlib.font_height

		fontlib = FontLib(self.__font_folder + 'font_50.bin')
		self.__content_buffer_dict = fontlib.get_characters(PerformanceMonitor.CHARS_MONITOR_CONTENT)
		self.__content_char_width = fontlib.font_width
		self.__content_char_height = fontlib.font_height

		del fontlib
		gc.collect()

	@timed_function
	def __initialize_groups(self):
		'''
		初始化 4 个信息窗格并计算更新内容区域
		'''
		self.__group_cpu = GroupInfo(0, self.__display, self.__header_title_char_width, self.__header_title_char_height, self.__content_char_width, self.__content_char_height)
		self.__group_ram = GroupInfo(1, self.__display, self.__header_title_char_width, self.__header_title_char_height, self.__content_char_width, self.__content_char_height)
		self.__group_hdd = GroupInfo(2, self.__display, self.__header_title_char_width, self.__header_title_char_height, self.__content_char_width, self.__content_char_height)
		self.__group_gpu = GroupInfo(3, self.__display, self.__header_title_char_width, self.__header_title_char_height, self.__content_char_width, self.__content_char_height)

		self.__display.bounding(True)

		self.__draw_group_content(self.__group_cpu)
		self.__group_cpu.bound = self.__display.bounding(True, True)

		self.__draw_group_content(self.__group_ram)
		self.__group_ram.bound = self.__display.bounding(True, True)

		self.__draw_group_content(self.__group_hdd)
		self.__group_hdd.bound = self.__display.bounding(True, True)

		self.__draw_group_content(self.__group_gpu)
		self.__group_gpu.bound = self.__display.bounding(True, True)

		self.__display.bounding(False)

		if DEBUG:
			print('=' * 30)
			print(f'cpu bound: {self.__group_cpu.bound}')
			print(f'ram bound: {self.__group_ram.bound}')
			print(f'hdd bound: {self.__group_hdd.bound}')
			print(f'gpu bound: {self.__group_gpu.bound}')
			print('=' * 30)

	@timed_function
	def __draw_time(self):
		x = self.__display_width - len(self.__last_time) * self.__time_percent_char_width - int(self.__time_percent_char_width / 2)
		y = int((PerformanceMonitor.HEADER_HEIGHT - self.__time_percent_char_height) / 2) + PerformanceMonitor.TIME_CONTENT_OFFSET_Y

		self.__draw_content(
			self.__last_time,
			self.__time_percent_buffer_dict,
			x, y,
			self.__time_percent_char_width,
			self.__time_percent_char_height,
			fore_color=st7789.BLACK, back_color=st7789.WHITE
		)

	# @timed_function
	def __draw_content(self,
		content:str,
		buffer_dict:dict,
		start_x:int, start_y:int,
		char_width:int, char_height:int,
		*, fore_color:int=st7789.WHITE, back_color:int=st7789.BLACK):
		'''
		绘制数据内容
		'''
		char_buffer_array = bytearray(char_width * char_height * 2)

		for char in content:
			char_buffer = buffer_dict[ord(char)]

			self.__display.map_bitarray_to_rgb565(
				char_buffer,
				char_buffer_array,
				char_width,
				fore_color, back_color
			)

			self.__display.blit_buffer(
				char_buffer_array,
				start_x, start_y,
				char_width,
				char_height
			)

			start_x += char_width

		gc.collect()

	@timed_function
	def __draw_group_content(self, group:GroupInfo, *, forecolor=st7789.WHITE, backcolor=st7789.BLACK):
		'''
		绘制窗格数据内容
		'''
		x = group.group_x + int((group.group_width - len(group.last_content) * group.content_char_width) / 2) + PerformanceMonitor.GROUP_CONTENT_OFFSET_X
		y = group.content_y

		char_buffer_array = bytearray(group.content_char_width * group.content_char_height * 2)

		for char in group.last_content:
			char_buffer = self.__content_buffer_dict[ord(char)]

			self.__display.map_bitarray_to_rgb565(
				char_buffer,
				char_buffer_array,
				group.content_char_width,
				forecolor, backcolor
			)

			self.__display.blit_buffer(
				char_buffer_array,
				x, y,
				group.content_char_width,
				group.content_char_height
			)

			x += group.content_char_width

		gc.collect()

	@timed_function
	def __draw_groups(self):
		'''
		绘制窗格及标题
		'''
		self.__display.rect(self.__group_cpu.group_x, self.__group_cpu.group_y, self.__group_cpu.group_width, self.__group_cpu.group_height, st7789.WHITE)
		self.__display.rect(self.__group_ram.group_x, self.__group_ram.group_y, self.__group_ram.group_width, self.__group_ram.group_height, st7789.WHITE)
		self.__display.rect(self.__group_hdd.group_x, self.__group_hdd.group_y, self.__group_hdd.group_width, self.__group_hdd.group_height, st7789.WHITE)
		self.__display.rect(self.__group_gpu.group_x, self.__group_gpu.group_y, self.__group_gpu.group_width, self.__group_gpu.group_height, st7789.WHITE)

		self.__draw_content('CPU', self.__header_title_buffer_dict, self.__group_cpu.title_x, self.__group_cpu.title_y, self.__group_cpu.title_char_width, self.__group_cpu.title_char_height)
		self.__draw_content('RAM', self.__header_title_buffer_dict, self.__group_ram.title_x, self.__group_ram.title_y, self.__group_ram.title_char_width, self.__group_ram.title_char_height)
		self.__draw_content('HDD', self.__header_title_buffer_dict, self.__group_hdd.title_x, self.__group_hdd.title_y, self.__group_hdd.title_char_width, self.__group_hdd.title_char_height)
		self.__draw_content('GPU', self.__header_title_buffer_dict, self.__group_gpu.title_x, self.__group_gpu.title_y, self.__group_gpu.title_char_width, self.__group_gpu.title_char_height)

		gc.collect()

	@timed_function
	def __draw_header(self):
		'''
		绘制标题栏及标题文字
		'''
		self.__display.fill_rect(
			0, 0,
			self.__display_width,
			PerformanceMonitor.HEADER_HEIGHT,
			st7789.WHITE
		)

		x = int((self.__display_width - len(PerformanceMonitor.CHARS_MONITOR_HEADER) * self.__header_title_char_width) / 2)
		y = int((PerformanceMonitor.HEADER_HEIGHT - self.__header_title_char_height) / 2)

		self.__draw_content(
			PerformanceMonitor.CHARS_MONITOR_HEADER,
			self.__header_title_buffer_dict,
			x, y,
			self.__header_title_char_width,
			self.__header_title_char_height,
			fore_color=st7789.BLACK, back_color=st7789.WHITE
		)

	def draw_background(self):
		'''
		绘制作为背景的标题栏和 4 个窗格
		'''
		self.__initialize_groups()
		self.__draw_header()
		self.__draw_groups()

	# @timed_function
	def update_values(self, cpu:str, cpu_temp:str, ram:str, hdd:str, gpu:str, time:str):
		'''
		更新数据内容
		'''
		if self.__group_cpu.last_content != cpu:
			if len(self.__group_cpu.last_content) > len(cpu):
				x, y, w, h = self.__group_cpu.bound
				self.__display.fill_rect(x, y, w, h, st7789.BLACK)

			self.__group_cpu.last_content = cpu
			forecolor = st7789.WHITE

			try:
				cpu_temp = int(cpu_temp)
				forecolor = st7789.WHITE if cpu_temp <= 55 else COLOR_ORANGE if cpu_temp <=75 else st7789.RED
			except ValueError:
				pass

			self.__draw_group_content(self.__group_cpu, forecolor=forecolor)
		
		if self.__group_ram.last_content != ram:
			if len(self.__group_ram.last_content) > len(ram):
				x, y, w, h = self.__group_ram.bound
				self.__display.fill_rect(x, y, w, h, st7789.BLACK)

			self.__group_ram.last_content = ram
			self.__draw_group_content(self.__group_ram)
		
		if self.__group_hdd.last_content != hdd:
			if len(self.__group_hdd.last_content) > len(hdd):
				x, y, w, h = self.__group_hdd.bound
				self.__display.fill_rect(x, y, w, h, st7789.BLACK)

			self.__group_hdd.last_content = hdd
			self.__draw_group_content(self.__group_hdd)
		
		if self.__group_gpu.last_content != gpu:
			if len(self.__group_gpu.last_content) > len(gpu):
				x, y, w, h = self.__group_gpu.bound
				self.__display.fill_rect(x, y, w, h, st7789.BLACK)

			self.__group_gpu.last_content = gpu
			self.__draw_group_content(self.__group_gpu)

		if self.__last_time != time:
			self.__last_time = time
			self.__draw_time()

	def __uart_receive_cb(self, _):
		if self.__uart.any():
			received_data = self.__uart.readline().strip(b'\r\n').strip(b'\n').strip(b'\r').strip()

			if not received_data:
				return

			if received_data in (b'\x03', b'\x04'):
				from machine import reset
				reset()

			if received_data.startswith(b'PM'):
				values = received_data.decode()[2:].split('||')
				self.update_values(values[0], values[1], values[2], values[3], values[4], values[5])

	def start(self):
		'''
		开启串口，接收服务端发送的数据内容
		'''
		if not self.__uart:
			self.__uart = UART(1, 115200, rx=Config.Pins.UART1_RX)
			self.__timer.init(
				mode=Timer.PERIODIC,
				period=100,
				callback=self.__uart_receive_cb
			)
	
	def stop(self):
		if self.__uart:
			self.__last_time = '00:00:00'
			self.__uart.deinit()
			self.__timer.deinit()

			self.__uart = None
		
		self.clean()

	def clean(self):
		self.__display.fill(st7789.BLACK)


def __button_irq_cb(pin_instance:Pin):
	global ROTATION

	ROTATION = (ROTATION + 1) % 4

	monitor.stop()
	display.rotation(ROTATION)
	monitor.draw_background()
	monitor.start()

@timed_function
def initialize_display():
	global ROTATION

	spi = SPI(
		1,
		baudrate=40000000,
		polarity=1,
		sck=Pin(Config.Pins.SCL),
		mosi=Pin(Config.Pins.SDA)
	)

	display = st7789.ST7789(
		spi, 240, 240,
		reset=Pin(Config.Pins.RST, Pin.OUT),
		dc=Pin(Config.Pins.DC, Pin.OUT)
	)
	display.init()
	display.rotation(ROTATION)

	gc.collect()

	return display


if __name__ == '__main__':
	if not ResourceManager.mount_resource_partition():
		print('Cannot mount resource partition')
		exit(0)

	display = initialize_display()
	button = Pin(Config.Pins.BOOT, Pin.IN, Pin.PULL_DOWN)

	monitor = PerformanceMonitor(
		display=display,
		font_folder=f'{RESOURCE_PARTITION}/fonts/'
	)

	button.irq(__button_irq_cb, Pin.IRQ_RISING)
	monitor.start()

	print('press ctrl+d or ctrl+c to restart')

	# monitor.update_values('20', '44', '30', '40', '50', '22:59')
	# utime.sleep(0.5)
	# monitor.update_values('100', '85', '100', '100', '100', '00:00')